Skip to content

IRIS CLI v1.2.1 — Fix bloqs create silent failure - #3

Merged
mayoalexander merged 24 commits into
mainfrom
dev
Apr 19, 2026
Merged

IRIS CLI v1.2.1 — Fix bloqs create silent failure#3
mayoalexander merged 24 commits into
mainfrom
dev

Conversation

@mayoalexander

Copy link
Copy Markdown

Summary

  • Fix bloqs create/get/ingest using wrong API route (/users//user/), causing 404s that appeared as silent "Failed" with no error detail
  • Stop spinner before displaying error messages so they're actually visible
  • Parse create response correctly (data.bloq not data directly)

Fixes #59970 — reported by external user (Patsy Keegan)

Test plan

  • iris bloqs create --name "Test" — creates successfully, shows ID and name
  • iris bloqs list — lists bloqs correctly
  • Build passes typecheck

🤖 Generated with Claude Code

mayoalexander and others added 24 commits April 18, 2026 08:20
…ime (#58778)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New platform-obs.ts with 10 subcommands:
- iris obs connect [ws://localhost:4455] — connect to OBS
- iris obs disconnect — disconnect
- iris obs scenes — list scenes (shows current)
- iris obs scene <name> — switch scene
- iris obs stream start|stop|status — streaming control
- iris obs record start|stop|status — recording control
- iris obs marker "description" — create timestamp marker
- iris obs mute <input> — toggle audio mute
- iris obs inputs — list audio/video sources
- iris obs status — full connection + stream + recording status

Bridge: OBS channel driver (channels/obs.js) using obs-websocket-js.
15 REST endpoints on bridge at /api/obs/*.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip interactive confirmation prompt when --force or -y is passed.
Enables tickets-push in scripts and CI without TTY.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
preflight: live system checks before going live (OBS connection, scenes,
stream/record status, cameras, audio, bridge health, ticket checkout URLs,
event page, venue, vendors). Cross-references event stages against OBS scenes.

audit: data completeness checks (title, description, dates, venue, address,
photo, stages, lineup, tickets priced/active/checkout URLs/QR codes, vendors,
landing page). Scores as percentage with actionable fix commands.

Both output grouped by category with ✓/✗ indicators and fix hints.
Exit non-zero when checks fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…787)

Backend already had both methods — CLI just wasn't listing them.
Now shows all 4 functions when running: iris integrations exec google-calendar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…3/#58784)

findLocalFile matched any {id}-*.json including tickets files.
Now excludes files containing 'tickets' in the name.
Fixes both: push defaulting to tickets data, and diff showing empty locals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…8785)

Instead of allowlisting specific fields, push now sends all fields.
Known fields go directly to the API. Unknown fields (twitter, custom
data, production notes) are merged into metadata so nothing is lost.
Roundtrip: pull → edit → push preserves everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows which non-schema fields went to metadata after push.
Transparency — user knows what's validated vs catch-all.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(#58884)

Default lookback increased from 7 to 30 days. New --since flag accepts
YYYY-MM-DD for absolute date control. Applies to both search and chats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- search: shows lead name instead of "Them" for incoming messages
- chats: resolves phone numbers → lead names in conversation list
- Searches leads by last 10 digits of phone number
- Falls back to raw identifier if no lead found

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New shared libraries:
- src/cli/lib/imessage.ts — isAvailable(), query(), searchByHandle(),
  listChats(), normalizeHandle(). Single source of truth for Messages.app SQLite.
- src/cli/lib/contacts.ts — resolveContactName(), resolveContactNames(),
  clearContactCache(). Per-session cached lead name resolution.

Refactored consumers:
- platform-doctor.ts: inline sqlite3 → isAvailable()
- platform-atlas-comms.ts: inline sqlite3 + MESSAGES_DB → searchByHandle()
- platform-leads.ts: inline sqlite3 health check → isAvailable()

139/139 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tacts libs

platform-imessage.ts:
- Removed MESSAGES_DB constant + queryMessages() function
- Uses isAvailable(), query(), normalizeHandle() from lib/imessage
- Uses resolveContactName/Names() from lib/contacts (was 20 lines inline)
- 3 platform checks simplified to isAvailable()

platform-customer.ts:
- queryMessagesDb() now delegates to lib/imessage query()
- Removed direct execSync + hardcoded DB path

All 5 files from REFACTOR_IMESSAGE_CONTACTS.md now use shared libs.
139/139 regression tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When searching by name (e.g. "Lisa Martinez"), the CLI now:
1. Searches leads API for matching contacts
2. Finds the first lead with a phone number → searches iMessage by that phone
3. Falls back to email if no phone (iMessage uses Apple ID = email)
4. Displays resolved contact name in results

Also fixed contacts.ts normalizeForSearch to strip +1 country code prefix
from E.164 format (+14695633672 → 4695633672) for consistent matching.

139/139 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New subcommand group for event production management:
- iris events production <id> overview — full production dashboard
- iris events production <id> runsheet — timeline with NOW/NEXT, --add, --done
- iris events production <id> checklist — todo list with completion %, --add, --done
- iris events production <id> budget — income/expenses/margin, --add-income, --add-expense

All data stored in metadata.production (no new tables).
Runsheet auto-populates from stage set_times + production_timeline if empty.

139/139 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (#58893)

Parses .vcf attachments from iMessage SQLite DB. Extracts name, phones,
emails, company. New command: iris imessage contacts [--days 90] [--chat ...]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Yargs positional args in parent commands conflict with subcommand routing.
Changed to -e/--event-id option. Usage: iris events production -e 1343 overview

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Primary email stays in email column. Additional emails stored in
contact_info.emails[] array. Improves merge detection — matching
on any email, not just primary.

Usage: iris leads create --name "Drew" --email drew@work.com --emails "drew@gmail.com,drew@other.com"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iris obs dashboard 1343     — opens in browser
iris obs dashboard 1343 --phone  — prints the phone URL (same WiFi)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One command shows all URLs:
- iris obs dashboard 1343 — opens browser + shows local/phone/public URLs
- iris obs dashboard 1343 --phone — LAN URL for same-WiFi devices
- iris obs dashboard 1343 --public — ngrok public URL (auto-starts if needed)
- iris obs dashboard 1343 --share "+15125551234" — sends URL via iMessage

Auto-starts ngrok tunnel if not running and ngrok is installed.
Detects public URL from ngrok API at localhost:4040.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ay (#59970)

All bloqs subcommands used `/api/v1/users/` (plural) but the Laravel route
is `/api/v1/user/` (singular), causing 404s that appeared as silent "Failed"
with no error detail.

- Fix URL path from `users` → `user` across all 6 bloqs subcommands
- Stop spinner before calling handleApiError so error messages render visibly
- Parse create response correctly (data.bloq, not data directly)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 19, 2026 14:49
@mayoalexander
mayoalexander merged commit 85b2fed into main Apr 19, 2026
0 of 6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to fix IRIS CLI Bloqs commands that were failing due to incorrect API routing and poor error visibility, but it also introduces several new CLI features (OBS control, iMessage utilities, and Events production checks) and accompanying regression tests.

Changes:

  • Fix Bloqs create/get/ingest endpoints to use /api/v1/user/{id}/... and ensure API errors are shown after stopping spinners.
  • Add shared iMessage + contact resolution libraries and expand iMessage CLI capabilities (search improvements, chat name resolution, vCard extraction).
  • Add new operational tooling (OBS CLI + Events preflight/audit/production commands) and new regression test suites.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
packages/opencode/src/cli/cmd/platform-bloqs.ts Fix Bloqs API routes and stop spinner before rendering API error details.
packages/opencode/package.json Bump CLI version to 1.2.1.
packages/opencode/src/cli/cmd/platform-run.ts Enhance array item display (calendar summary + start time) and add calendar integration functions.
packages/opencode/test/cli/bug-fixes.test.ts Add tests for calendar event label + start time parsing logic.
packages/opencode/test/platform/bug-regressions.test.ts Add broad regression suite asserting fixes across multiple platform commands.
packages/opencode/src/cli/lib/imessage.ts New shared sqlite-based iMessage access utilities + vCard extraction.
packages/opencode/src/cli/lib/contacts.ts New shared lead-name resolution + caching for phone/email identifiers.
packages/opencode/src/cli/cmd/platform-imessage.ts Refactor to shared libs; add --since, lead resolution, chat labeling, and contacts subcommand.
packages/opencode/src/cli/cmd/platform-leads.ts Add alt-email support in lead create; reuse iMessage availability checker in health checks.
packages/opencode/src/cli/cmd/platform-doctor.ts Reuse shared iMessage availability checker for Full Disk Access check.
packages/opencode/src/cli/cmd/platform-customer.ts Switch iMessage DB querying to shared lib.
packages/opencode/src/cli/cmd/platform-atlas-comms.ts Switch iMessage ingestion to shared lib helpers.
packages/opencode/src/cli/cmd/platform-obs.ts New OBS control command suite (bridge-based) including dashboard share/open helpers.
packages/opencode/src/index.ts Register new obs command in the CLI.
packages/opencode/src/cli/cmd/command-groups.ts Categorize obs command for grouped help.
packages/opencode/src/cli/cmd/platform-events.ts Expand Events tooling: push metadata pass-through, tickets push --force, preflight/audit commands, and production subcommand registration.
packages/opencode/src/cli/cmd/platform-events-production.ts New production subcommands (overview/runsheet/checklist/budget) stored in event metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +87
export function searchByHandle(handle: string, days = 30, limit = 50): Message[] {
const search = normalizeHandle(handle)
const cutoffSeconds = days * 86400

const sql = `SELECT
m.rowid, m.text, m.is_from_me, m.date,
datetime(m.date/1000000000 + strftime('%s','2001-01-01'), 'unixepoch', 'localtime') as sent_dt,
c.chat_identifier
FROM message m
JOIN chat_message_join cmj ON m.rowid = cmj.message_id
JOIN chat c ON cmj.chat_id = c.rowid
WHERE c.chat_identifier LIKE '%${search}%'
AND m.text IS NOT NULL AND m.text != ''
AND m.date > (strftime('%s','now') - ${cutoffSeconds} - strftime('%s','2001-01-01')) * 1000000000
ORDER BY m.date DESC LIMIT ${limit}`

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

searchByHandle() interpolates search directly into the SQL LIKE '%${search}%' without escaping single quotes. If handle is an email/chat identifier containing ', the query will break (and could be leveraged for SQL injection inside sqlite). Escape ' as '' (as you already do elsewhere) before building the SQL, or parameterize the query.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +110
// Resolve contact name from leads (#58888)
const contactName = await resolveContactName(digits || String(args.query)) ?? "Them"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contactName resolution uses digits || args.query, but digits is derived from the original query. If the query is a lead name that gets resolved to a phone/email (lines 35-58), digits will still be empty and you'll end up resolving by the name string instead of the resolved identifier. Track the resolved identifier (e.g. resolved phone/email) and pass that into resolveContactName(...) so the displayed sender label matches the chat handle actually queried.

Suggested change
// Resolve contact name from leads (#58888)
const contactName = await resolveContactName(digits || String(args.query)) ?? "Them"
// Resolve contact name from the actual identifier used for the query (#58888)
const contactName = await resolveContactName(resolvedIdentifier) ?? "Them"

Copilot uses AI. Check for mistakes.
Comment on lines +372 to +377
try {
const { execSync } = await import("child_process")
const handle = String(args.share)
const msg = `🎬 Stream Control Dashboard — open this link:\n\n${shareUrl}\n\nTap scenes to switch cameras. Timeline tab for run-of-show.`
execSync(`osascript -e 'tell application "Messages" to send "${msg.replace(/"/g, '\\"')}" to participant "${handle}" of (1st account whose service type = iMessage)'`, { timeout: 10000 })
console.log(` ${success("✓")} Sent to ${handle}`)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --share path builds an osascript command string using execSync(...) and interpolates both handle (user input) and msg into a single-quoted shell command. This is vulnerable to shell/AppleScript injection (e.g. a handle containing ' breaks quoting) and can also fail on normal characters. Use execFileSync("osascript", ["-e", script]) (no shell) and escape values for AppleScript (or pass them as arguments) rather than concatenating into the command string.

Copilot uses AI. Check for mistakes.
Comment on lines 1334 to +1343
// iMessage — verify macOS Messages.app SQLite access
(async (): Promise<ChannelHealth> => {
try {
const { execSync } = await import("child_process")
const { homedir } = await import("os")
const db = `${homedir()}/Library/Messages/chat.db`
execSync(`sqlite3 "${db}" "SELECT count(*) FROM message LIMIT 1"`, { encoding: "utf-8", timeout: 3000 })
return { name: "iMessage", ok: true, status: "verified" }
} catch (e: any) {
const msg = e?.message ?? ""
if (msg.includes("not authorized") || msg.includes("permission denied")) {
return { name: "iMessage", ok: false, status: "no_permission", error: "Full Disk Access required", hint: "System Settings → Privacy → Full Disk Access → enable terminal" }
const { isAvailable } = await import("../lib/imessage")
if (isAvailable()) {
return { name: "iMessage", ok: true, status: "verified" }
}
return { name: "iMessage", ok: false, status: "error", error: "SQLite access failed", hint: "check macOS Messages.app" }
return { name: "iMessage", ok: false, status: "no_permission", error: "Full Disk Access required", hint: "System Settings → Privacy → Full Disk Access → enable terminal" }
} catch {
return { name: "iMessage", ok: false, status: "error", error: "check failed", hint: "check macOS Messages.app" }

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iMessage health check treats any isAvailable() === false as status: "no_permission" with a Full Disk Access hint. isAvailable() also returns false on non-macOS and when the DB file doesn't exist, so this will misreport the failure mode (e.g. Linux users will be told to enable Full Disk Access). Consider branching on process.platform !== "darwin" / missing DB separately (e.g. status: "error" with "macOS only" / "Messages DB not found") and reserve no_permission for actual permission errors.

Copilot uses AI. Check for mistakes.

// ============================================================================
// Event Production CLI — runsheet, checklist, budget, overview
// All data in metadata.production (no new tables)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File header says “All data in metadata.production”, but the implementation also reads event.metadata.production_timeline / local.data.metadata.production_timeline as a separate top-level metadata field. Either move that timeline under metadata.production (to match the statement) or update the comment so it accurately describes where production data lives.

Suggested change
// All data in metadata.production (no new tables)
// Production data lives in metadata.production; timeline data may also be read
// from metadata.production_timeline (no new tables)

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +58
export function isAvailable(): boolean {
if (process.platform !== "darwin") return false
if (!existsSync(MESSAGES_DB)) return false
try {
execSync(`sqlite3 "${MESSAGES_DB}" "SELECT 1 FROM message LIMIT 1"`, {
encoding: "utf-8",
timeout: 3000,
})
return true
} catch {
return false
}
}

/**
* Run a raw SQL query against the Messages database.
* Escapes double quotes in the SQL string.
*/
export function query(sql: string): string {
const escaped = sql.replace(/"/g, '\\"')
return execSync(`sqlite3 "${MESSAGES_DB}" "${escaped}"`, {
encoding: "utf-8",
timeout: 10000,
}).trim()
}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isAvailable()/query() use execSync() with a shell command string (e.g. sqlite3 "${MESSAGES_DB}" "${escaped}"). Because the SQL text can include user-derived values in callers, shell expansions like $(...)/backticks can be evaluated before sqlite3 runs, creating a command-injection risk and also making quoting brittle. Use execFileSync/spawnSync with an argument array (no shell) and pass the SQL as a separate argv element or via stdin; also consider rejecting/escaping characters like $ and backticks if any user input can reach SQL.

Copilot uses AI. Check for mistakes.
Comment on lines 136 to +162
builder: (yargs) =>
yargs
.positional("query", { type: "string", demandOption: true, describe: "phone number or chat identifier" })
.option("last", { type: "number", default: 10, describe: "number of recent messages" })
.option("days", { type: "number", default: 30, describe: "search last N days" })
.option("json", { type: "boolean", default: false }),
async handler(args) {
UI.empty()
prompts.intro(`◈ iMessage Read — "${args.query}"`)

if (process.platform !== "darwin" || !existsSync(MESSAGES_DB)) {
prompts.log.error("iMessage database not available")
if (!isAvailable()) {
prompts.log.error("iMessage not available. Requires macOS + Full Disk Access.")
prompts.outro("Done")
return
}

const digits = args.query.replace(/\D/g, "")
const isPhone = digits.length >= 7
const normalized = isPhone ? normalizeHandle(args.query) : args.query
const whereClause = isPhone
? `c.chat_identifier LIKE '%${digits.slice(-10)}%'`
? `c.chat_identifier LIKE '%${normalized}%'`
: `c.chat_identifier LIKE '%${args.query.replace(/'/g, "''")}%'`

const cutoffSeconds = args.days * 86400
// --since takes priority over --days (#58884)
const cutoffSeconds = args.since
? Math.max(0, Math.floor((Date.now() - new Date(String(args.since)).getTime()) / 1000))
: (args.days as number) * 86400

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImessageReadCommand uses args.since to compute cutoffSeconds, but the command builder never defines a --since option. As written, --since won't be recognized by yargs and the new logic is effectively dead code. Add the .option("since", ...) to the builder (matching search/chats) or remove the args.since branch here.

Copilot uses AI. Check for mistakes.
Comment on lines +424 to +425
const { exec } = await import("child_process")
exec(`open "${openUrl}"`)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec(open "${openUrl}") executes through a shell and openUrl is derived from network sources (ngrok API) and local IP detection. If the URL ever contains quotes or shell metacharacters, this can lead to command injection or just broken behavior. Prefer execFile/spawn with args (e.g. open, [openUrl]) so the URL is passed as a single argument.

Suggested change
const { exec } = await import("child_process")
exec(`open "${openUrl}"`)
const { spawn } = await import("child_process")
spawn("open", [openUrl], { detached: true, stdio: "ignore" }).unref()

Copilot uses AI. Check for mistakes.
const whereClause = isPhone
? `c.chat_identifier LIKE '%${digits.slice(-10)}%'`
? `c.chat_identifier LIKE '%${normalized}%'`
: `c.chat_identifier LIKE '%${args.query.replace(/'/g, "''")}%'`

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In search, when a non-phone query resolves to a lead email (lines 54-58), normalized is updated but whereClause still uses args.query in the non-phone branch. That means the email resolution has no effect on the actual SQLite query (it still searches for the original name string). Use the resolved identifier (normalized) in the LIKE clause for the non-phone case as well (with proper SQL escaping), or introduce a separate searchHandle variable that always reflects what you’re querying for.

Suggested change
: `c.chat_identifier LIKE '%${args.query.replace(/'/g, "''")}%'`
: `c.chat_identifier LIKE '%${normalized.replace(/'/g, "''")}%'`

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +72
const cutoffSeconds = args.since
? Math.max(0, Math.floor((Date.now() - new Date(String(args.since)).getTime()) / 1000))
: (args.days as number) * 86400

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--since is parsed via new Date(String(args.since)).getTime() without validating the result. If the user passes an invalid date (or a timezone-less string that parses unexpectedly), getTime() becomes NaN and you’ll emit unixepoch('now') - NaN in SQL, which fails with a confusing sqlite error. Validate Number.isFinite(ts) and surface a clear error (and non-zero exit code) when --since can’t be parsed.

Suggested change
const cutoffSeconds = args.since
? Math.max(0, Math.floor((Date.now() - new Date(String(args.since)).getTime()) / 1000))
: (args.days as number) * 86400
let cutoffSeconds = (args.days as number) * 86400
if (args.since) {
const ts = new Date(String(args.since)).getTime()
if (!Number.isFinite(ts)) {
prompts.log.error(`Invalid value for --since: "${String(args.since)}". Please use a valid date/time.`)
process.exitCode = 1
prompts.outro("Done")
return
}
cutoffSeconds = Math.max(0, Math.floor((Date.now() - ts) / 1000))
}

Copilot uses AI. Check for mistakes.
mayoalexander added a commit that referenced this pull request Aug 19, 2026
…ed on a 404

`iris integrations list` collapsed every probe failure into "unverified — could
not probe", which tells the reader nothing and cost real debugging hours.

Two worse bugs surfaced while fixing that:

1. The gmail and google-drive probes returned "verified" for ANY response that
   was not 401/403 — including 404 and 500. Both probe endpoints currently 404
   (/api/v1/leads/0/gmail-threads, /api/v1/integrations/exec), so Gmail has been
   rendering a green [verified] while its probe does not resolve at all. The
   health signal on those rows was meaningless.

2. Probe results were keyed by integration TYPE, not id, and the drive probe
   never passed integration_id. All three google-drive accounts (#4, #11, #12)
   therefore shared one result taken from whichever connection the API picks by
   default — two accounts could be dead and every row would show the third's
   status. The comment directly above that code says multi-account visibility is
   the point of the command.

Changes:

- Probes return { state, reason, fix } instead of a bare string, so every row
  states its cause and, where one exists, the command that fixes it.
- New "unknown" state, distinct from failure. A 404 on the PROBE path means we
  cannot determine the integration's health; claiming either verified or
  unverified would be unsupported. Rendered neutral rather than red, because the
  fix belongs to us, not the user.
- Network failures are classified — connection refused / timeout / DNS — rather
  than collapsing into one string.
- Results keyed by integration id; the drive probe passes integration_id so each
  account is genuinely probed.
- The calendar probe distinguishes bridge-not-running, no-bridge-key,
  key-rejected and HTTP-n, each with `iris hive doctor` as the fix.

    before  gmail #3            [verified]
    after   gmail #3            [unknown] — probe endpoint returned 404; status not determined
    before  google-calendar #2  [unverified] — could not probe
    after   google-calendar #2  [unverified] — no bridge key configured → iris hive doctor

Verified by running the command from source, not by reading the diff. Typecheck
clean; the remaining tsc error (session/llm.ts:88 TS2589) is pre-existing and
reproduces with this change stashed.

Refs #180929, #181016

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mayoalexander added a commit that referenced this pull request Aug 30, 2026
…ust that it failed (#182862)

The ticket says --account "fails to resolve existing connections". It does not. Reproduced on
the reporting account:

  alex@freelabel.net  is a GOOGLE-DRIVE account (#4)
  the two GMAIL connections are #3 (no account_email at all) and #14 (admin@vanguardhcs.com)

So there is genuinely no gmail connection at that address, and the resolution was correct. The
defect is the sentence it produced:

  No connection for type='gmail' matching --account='alex@freelabel.net'. Run: iris integrations list

The user runs that, sees Gmail plainly connected, and concludes the flag is broken. The message
described the outcome instead of the evidence, and sent them somewhere that contradicted it.

It now lists the connections of that type, with their ids and addresses, and says which of them
can NEVER match because no account_email is stored — 21 of 25 connections on this account carry
none, so an email match against them cannot succeed and no amount of retrying will help. The fix
is --integration-id or a reconnect, and neither was guessable before.

"Nothing matched" and "nothing is connected" are now different sentences with different fixes.

ALSO, one layer down: `if (!res.ok) return null` meant a 500 on OUR lookup endpoint rendered as
"no connection" — the same conflation as #182861, where a failure in our path became a claim
about the user's integration. That now says the lookup failed and that it implies nothing about
whether the integration is connected.

describeAccountMiss is extracted and exported because the bug was in the sentence, so the
sentence is the thing worth pinning. 5 tests, no transport needed. Typechecked: zero diagnostics
in platform-run.ts (TS2589 in session/llm.ts is pre-existing).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDsGZrjZEh5f2GTtZj9oBK
mayoalexander added a commit that referenced this pull request Sep 8, 2026
`OutreachScriptTemplate` is referenced by every outreach step (`script_template_id`) and
stamped on every comm for attribution, has server-side versioning with activate and
rollback, and is fully routed — 15 endpoints under
/api/v1/bloqs/{bloq}/outreach-script-templates including a whole /versions subtree.

None of it was reachable. The only way to touch a script was `iris som edit <campaign>`
inline, which meant scripts could only be edited through the campaign path even though
strategies depend on them equally, and the version history could never be listed, snapshotted
or rolled back from the CLI at all.

  iris reachr script list|show|create|update|delete <bloq> [id]
  iris reachr script versions|snapshot|activate <bloq> <id>     (activate aliases rollback)

Two things found by running it rather than reading it:

- `snapshot` posting only a label got a 500 "Failed to add version". The endpoint requires
  `message` and sets the live text while snapshotting it, so "snapshot the current message"
  means reading it first and sending it back unchanged. The server laundering a validation
  error into a generic 500 is filed separately.

- `activate <bloq> <id> <version>` always sent version 0. A positional named `version` is
  shadowed by the CLI's global `-v, --version`, so args.version was the flag, not the
  argument, and the server answered "Version 0 not found" — an error that names the symptom
  and nothing about where the 0 came from. The positional is `<n>`.

Also warns locally on merge tokens nothing will substitute, before the request goes out.
{sender_name} shipped to a real inbox last week because that check existed nowhere.

Verified against the live API: created script #3 on board 657, snapshotted v1, edited,
snapshotted v2, rolled back to v1 and confirmed the original text returned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDsGZrjZEh5f2GTtZj9oBK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants